Conversation
Move CircleBadge component from TimetableScene to SharedUIComponents module for reusability across features. Update TimetableMenuSection to import from the new shared location.
- Apply cyan tint to edit/save buttons in lecture detail toolbar - Apply cyan tint to sheet top bar buttons - Fix lecture color resolution to respect custom colors for unsaved lectures
시간표 그리드의 시간 행 렌더링을 위한 높이와 개수를 계산하는 로직을 getDisplayGridMetrics() 메서드로 추출하여 중복 제거 및 유지보수성 개선
시간표 그리드 메트릭스 계산 로직에 대한 단위 테스트 추가: - Safe area insets가 있는 경우의 계산 검증 - 18시간 이상 범위에서의 헤더 잘림 방지 검증 - 최소 시간 높이(10pt) 제한 검증
- Use hairline-width dividers (1.0/displayScale) for pixel-perfect rendering - Adjust divider colors for better visual hierarchy - Extract reusable Divider component in TimetableGridLayer - Add timetable.background color asset to Timetable and Friends modules - Apply background color to timetable views in both features
Store fetched themes in UserDefaults to make them available in widget extension context. Widgets now correctly display lecture colors by loading cached themes.
…dgetView Separated timetable UI components to better distinguish between app and widget usage: - Renamed TimetableZStack to TimetableView for app usage - Created TimetableWidgetView for widget-specific rendering - Updated all references in TimetableScene, LectureSearchScene, and TimetableUIProvider - Updated widget to use TimetableWidgetView with proper environment values - Cleaned up CLAUDE.md formatting
Add WidgetReloader dependency to automatically reload widgets when timetable or theme data changes in UserDefaults repositories. This ensures widgets stay in sync with app data without manual refresh.
There was a problem hiding this comment.
Pull request overview
Widget/Timetable UI에서 보이는 렌더링/테마/배경 관련 이슈를 수정하고, 위젯 리로드 및 테마 로컬 저장을 추가한 PR입니다.
Changes:
- 위젯 시간표 렌더링 개선(새
TimetableWidgetView, 그리드 라인/시간 라벨 계산 보정, 강의 색상 반영) - 위젯/화면 배경 및 tint 일관화, 공용 UI 컴포넌트 분리(
CircleBadge,Divider) - 테마 목록 로컬 저장 및 변경 시 위젯 타임라인 리로드 추가
Reviewed changes
Copilot reviewed 31 out of 31 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| SNUTT/SNUTTWidget/Sources/UI/TimetableFullWidgetView.swift | 위젯 풀 사이즈에서 TimetableWidgetView로 래핑 전환 |
| SNUTT/SNUTTWidget/Sources/UI/TimetableCompactWidgetView.swift | 위젯 컴팩트 뷰에 painter 전달하여 강의 색상 표시 |
| SNUTT/SNUTTWidget/Sources/UI/TimetableAccessoryRectangularView.swift | 액세서리 위젯에 painter 전달하여 강의 색상 표시 |
| SNUTT/SNUTTWidget/Sources/SNUTTWidgetDataSource.swift | 위젯에서 사용할 available themes 로드 추가 |
| SNUTT/SNUTTWidget/Sources/SNUTTWidget.swift | 엔트리에 available themes 포함 및 painter 생성 로직 복구 |
| SNUTT/Project.swift | DependenciesUtility 타겟 추가 및 WidgetKit 링크 보강 |
| SNUTT/Modules/Utility/DependenciesUtility/Sources/WidgetReloaderDependency.swift | 위젯 리로드 의존성(DependencyKey) 추가 |
| SNUTT/Modules/Shared/SharedUIComponents/Sources/Sheet/SheetTopBar.swift | TopBar tint 일관화 |
| SNUTT/Modules/Shared/SharedUIComponents/Sources/CircleBadge.swift | CircleBadge 공용 컴포넌트로 분리 |
| SNUTT/Modules/Feature/TimetableUIComponents/Tests/TimetablePainterTests.swift | 그리드 메트릭 계산 관련 테스트 추가 |
| SNUTT/Modules/Feature/TimetableUIComponents/Sources/UI/TimetableWidgetView.swift | 위젯 전용 시간표 렌더링 뷰 추가 |
| SNUTT/Modules/Feature/TimetableUIComponents/Sources/UI/TimetableView.swift | TimetableZStack → TimetableView로 rename |
| SNUTT/Modules/Feature/TimetableUIComponents/Sources/UI/TimetableGridLayer.swift | hairline/divider 및 표시 그리드 메트릭 반영 |
| SNUTT/Modules/Feature/TimetableUIComponents/Sources/Presentation/TimetablePainter.swift | 그리드 표시용 메트릭 계산 함수 추가 |
| SNUTT/Modules/Feature/TimetableUIComponents/Sources/Presentation/TimetablePainter+Theme.swift | 강의 customColor 기본 반영 보강 |
| SNUTT/Modules/Feature/TimetableUIComponents/Resources/Assets.xcassets/Contents.json | assets contents 포맷 정리 |
| SNUTT/Modules/Feature/Timetable/Sources/UI/TimetableUIProvider.swift | TimetableView로 교체 |
| SNUTT/Modules/Feature/Timetable/Sources/UI/TimetableScene.swift | 배경 컬러 적용 및 Divider/CircleBadge 정리 |
| SNUTT/Modules/Feature/Timetable/Sources/UI/TimetableMenu/TimetableMenuSection.swift | SharedUIComponents 임포트 추가 |
| SNUTT/Modules/Feature/Timetable/Sources/UI/LectureSearchScene.swift | TimetableView로 교체 및 포커스/툴바 일부 정리 |
| SNUTT/Modules/Feature/Timetable/Sources/UI/LectureEditDetail/LectureEditDetailScene+Toolbar.swift | 툴바 버튼 tint 일관화 |
| SNUTT/Modules/Feature/Timetable/Sources/Infra/TimetableUserDefaultsRepository.swift | 설정/선택 시간표 저장 시 위젯 리로드 |
| SNUTT/Modules/Feature/Timetable/Resources/Assets.xcassets/timetable.background.colorset/Contents.json | 시간표 배경 컬러 asset 추가 |
| SNUTT/Modules/Feature/ThemesInterface/Sources/ThemeLocalRepository.swift | 테마 로컬 저장소 프로토콜/의존성 키 추가 |
| SNUTT/Modules/Feature/Themes/Sources/Presentation/ThemeViewModel.swift | 테마 fetch 후 로컬 저장 추가 |
| SNUTT/Modules/Feature/Themes/Sources/Infra/ThemeUserDefaultsRepository.swift | 테마 로컬 저장(UserDefaults) 및 위젯 리로드 |
| SNUTT/Modules/Feature/Themes/Sources/Composition/LiveDependencies.swift | ThemeLocalRepository live dependency 등록 |
| SNUTT/Modules/Feature/Settings/Sources/UI/Timetable/TimetableSettingView.swift | 설정 화면 tint 일관화 |
| SNUTT/Modules/Feature/Friends/Sources/UI/FriendsScene.swift | 친구 시간표 화면 배경 및 Divider 추가 |
| SNUTT/Modules/Feature/Friends/Resources/Assets.xcassets/timetable.background.colorset/Contents.json | 친구 시간표 배경 컬러 asset 추가 |
| SNUTT/CLAUDE.md | 가이드 문구 일부 제거(코드 변경 아님) |
SNUTT/Modules/Shared/SharedUIComponents/Sources/CircleBadge.swift
Outdated
Show resolved
Hide resolved
SNUTT/Modules/Feature/TimetableUIComponents/Sources/Presentation/TimetablePainter.swift
Outdated
Show resolved
Hide resolved
| var availableThemes: [Theme] { | ||
| guard let data = userDefaults.data(forKey: "availableThemes"), | ||
| let themes = try? JSONDecoder().decode([Theme].self, from: data) | ||
| else { return [] } | ||
| return themes | ||
| } |
There was a problem hiding this comment.
availableThemes UserDefaults 키를 문자열 리터럴로 직접 사용하고 있어(예: \"availableThemes\") 다른 저장소(예: ThemeUserDefaultsRepository.Keys.availableThemes)와 드리프트 위험이 있습니다. 키를 shared constant/enum(예: Keys.availableThemes.rawValue)로 중앙화해서 재사용하는 형태로 맞추는 것을 권장합니다.
|
|
||
| public init(reloadAll: @escaping @Sendable () -> Void) { | ||
| self.reloadAll = reloadAll | ||
| } | ||
| } | ||
|
|
||
| public enum WidgetReloaderKey: DependencyKey { | ||
| public static let liveValue: WidgetReloader = .init( | ||
| reloadAll: { WidgetCenter.shared.reloadAllTimelines() } | ||
| ) | ||
|
|
||
| public static let testValue: WidgetReloader = .init(reloadAll: {}) |
There was a problem hiding this comment.
현재 WidgetReloader가 reloadAll만 제공해서 호출 측이 항상 WidgetCenter.shared.reloadAllTimelines()에 종속됩니다. 위젯 종류가 특정 가능하다면 reload(kind:) 또는 reloadTimelines(ofKind:) 같은 더 좁은 API도 함께 제공해 불필요한 전체 리로드를 피할 수 있게 해주세요.
| public init(reloadAll: @escaping @Sendable () -> Void) { | |
| self.reloadAll = reloadAll | |
| } | |
| } | |
| public enum WidgetReloaderKey: DependencyKey { | |
| public static let liveValue: WidgetReloader = .init( | |
| reloadAll: { WidgetCenter.shared.reloadAllTimelines() } | |
| ) | |
| public static let testValue: WidgetReloader = .init(reloadAll: {}) | |
| public let reloadTimelinesOfKind: @Sendable (_ kind: String) -> Void | |
| public init( | |
| reloadAll: @escaping @Sendable () -> Void, | |
| reloadTimelinesOfKind: @escaping @Sendable (_ kind: String) -> Void | |
| ) { | |
| self.reloadAll = reloadAll | |
| self.reloadTimelinesOfKind = reloadTimelinesOfKind | |
| } | |
| @inlinable | |
| public func reloadTimelines(ofKind kind: String) { | |
| reloadTimelinesOfKind(kind) | |
| } | |
| } | |
| public enum WidgetReloaderKey: DependencyKey { | |
| public static let liveValue: WidgetReloader = .init( | |
| reloadAll: { WidgetCenter.shared.reloadAllTimelines() }, | |
| reloadTimelinesOfKind: { kind in | |
| WidgetCenter.shared.reloadTimelines(ofKind: kind) | |
| } | |
| ) | |
| public static let testValue: WidgetReloader = .init( | |
| reloadAll: {}, | |
| reloadTimelinesOfKind: { _ in } | |
| ) |
| func storeSelectedTimetable(_ timetable: Timetable) throws { | ||
| let data = try JSONEncoder().encode(timetable) | ||
| userDefaults.set(data, forKey: Keys.currentTimetable.rawValue) | ||
| widgetReloader.reloadAll() | ||
| } |
There was a problem hiding this comment.
UserDefaults 저장 시마다 widgetReloader.reloadAll()로 전체 위젯 타임라인을 리로드하고 있습니다. 저장이 빈번할 수 있는 경로라면, (1) 특정 kind만 리로드하거나, (2) 변경을 debounce/throttle하는 방식으로 리로드 비용/레이트리밋 리스크를 줄이는 것을 권장합니다.
SNUTT/Modules/Feature/TimetableUIComponents/Sources/Presentation/TimetablePainter.swift
Show resolved
Hide resolved
Extract Xcode version to environment variable for easier maintenance across CI and deployment workflows.
…t access Move UserDefaults key definitions from repository implementations to interface modules, enabling widgets to access the same keys without duplicating magic strings. Also fix CircleBadge color parameter and remove typo in TimetablePainter comment.
| public init() {} | ||
| public var body: some View { | ||
| Rectangle() | ||
| .fill(Design.dividerColor) |
There was a problem hiding this comment.
TimetableGridLayer.Divider는 TimetableGridLayer의 중첩 타입 Design을 현재 스코프에서 직접 참조하고 있어 컴파일 에러가 날 가능성이 큽니다(중첩 타입 내부에서는 외부 타입의 멤버가 자동으로 스코프에 들어오지 않음). TimetableGridLayer.Design.dividerColor처럼 타입을 명시해서 참조하거나, Divider를 TimetableGridLayer 내부에 중첩 타입으로 두어 스코프를 명확히 해주세요.
| .fill(Design.dividerColor) | |
| .fill(TimetableGridLayer.Design.dividerColor) |
| TimetableGridLayer(painter: painter, geometry: geometry) | ||
| .frame(width: geometry.size.width, height: geometry.extendedContainerSize.height) | ||
| TimetableBlocksLayer(painter: painter, geometry: geometry) | ||
| .frame(width: geometry.size.width, height: geometry.size.height) |
There was a problem hiding this comment.
TimetableWidgetView에서 Grid는 extendedContainerSize.height, Blocks는 size.height로 서로 다른 높이를 사용하고 있어(특히 safeAreaInsets가 있는 경우) 레이어 정렬/클리핑이 일관되지 않을 수 있습니다. 위젯에서는 스크롤이 없으므로 두 레이어가 같은 기준 높이를 쓰도록 맞추거나, 의도적으로 다르게 둔 이유를 주석/구조로 명확히 해두는 편이 안전합니다.
| .frame(width: geometry.size.width, height: geometry.size.height) | |
| .frame(width: geometry.size.width, height: geometry.extendedContainerSize.height) |
그냥 써보면서 눈에 보이는 이슈들 수정